<?php
/**
 * Created by PhpStorm.
 * User: whwyy
 * Date: 2018/3/30 0030
 * Time: 14:39
 */

namespace BeReborn\Database\Base;


use BeReborn\Base\Component;
use BeReborn\Database\ActiveQuery;
use BeReborn\Database\ActiveRecord;
use BeReborn\Database\Connection;
use BeReborn\Database\HasMany;
use BeReborn\Database\HasOne;
use BeReborn\Database\IOrm;
use BeReborn\Database\Mysql\Columns;
use BeReborn\Database\Relation;
use BeReborn\Error\Logger;
use BeReborn\Exception\DbException;
use BeReborn\Http\Context;
use Exception;
use validator\Validator;

/**
 * Class BOrm
 *
 * @package BeReborn\Base
 *
 * @property bool $isCreate
 * @method rules()
 * @method static tableName()
 */
abstract class BaseActiveRecord extends Component implements IOrm, \ArrayAccess
{

    /** @var array */
    protected $_attributes = [];

    /** @var array */
    protected $_oldAttributes = [];

    /** @var array */
    protected $_relate = [];

    /** @var null|string */
    protected static $primary = NULL;

    /**
     * @var bool
     */
    protected $isNewExample = TRUE;

    protected $actions = [];

    /** @var Relation */
    protected $_relation = [];

    /**
     * @throws Exception
     */
    public function init()
    {
        if (!Context::hasContext(Relation::class)) {
            Context::setContext(Relation::class, \BeReborn::createObject(Relation::class));
        }
        $this->_relation = Context::getContext(Relation::class);
    }

    /**
     * @param string $column
     * @param int $value
     * @return void
     * @throws Exception
     */
    public function incrBy(string $column, int $value)
    {
        throw new Exception('Undefined function incrBy in ' . get_called_class());
    }

    /**
     * @param string $column
     * @param int $value
     * @return void
     * @throws Exception
     */
    public function decrBy(string $column, int $value)
    {
        throw new Exception('Undefined function decrBy in ' . get_called_class());
    }

    /**
     * @return array
     */
    public function getActions()
    {
        return $this->actions;
    }

    /**
     * @return bool
     */
    public function getIsCreate()
    {
        return $this->isNewExample === TRUE;
    }

    /**
     * @param bool $bool
     * @return $this
     */
    public function setIsCreate($bool = FALSE)
    {
        $this->isNewExample = $bool;
        return $this;
    }

    /**
     * @return mixed
     *
     * get last exception or other error
     */
    public function getLastError()
    {
        return Logger::getLastError('mysql');
    }

    /**
     * @return bool
     * @throws Exception
     */
    public static function hasPrimary(): bool
    {
        if (static::$primary !== NULL) {
            return true;
        }
        $primary = static::getColumns()->getPrimaryKeys();
        if (!empty($primary)) {
            return true;
        }
        return false;
    }

    /**
     * @throws Exception
     */
    public function hasAutoIncrement()
    {
        $autoIncrement = $this->getAutoIncrement();
        return $autoIncrement !== null;
    }

    /**
     * @throws Exception
     */
    public function getAutoIncrement()
    {
        return static::getColumns()->getAutoIncrement();
    }

    /**
     * @return null|string
     * @throws Exception
     */
    public static function getPrimary()
    {
        if (!static::hasPrimary()) {
            return null;
        }
        if (!empty(static::$primary)) {
            return static::$primary;
        }
        $primary = static::getColumns()->getPrimaryKeys();
        if (!empty($primary)) {
            return is_array($primary) ? current($primary) : $primary;
        }
        return null;
    }

    /**
     * @param $condition
     * @param $db
     * @return $this
     * @throws
     */
    public static function findOne($condition, $db = NULL)
    {
        if (empty($condition) || !is_numeric($condition)) {
            return NULL;
        }
        return static::find()->where([static::getPrimary() => $condition])->first();
    }

    /**
     * @param null $field
     * @return ActiveRecord|null
     * @throws DbException
     * @throws Exception
     */
    public static function max($field = null)
    {
        if (empty($field)) {
            $field = self::getPrimary();
        }

        $columns = static::getColumns()->get_fields(static::getDbName());
        if (!isset($columns[$field])) {
            return null;
        }

        $first = static::find()->max($field)->first();
        if (empty($first)) {
            return null;
        }
        return $first[$field];
    }

    /**
     * @return mixed|ActiveQuery
     * @throws
     */
    public static function find()
    {
        return \BeReborn::createObject(ActiveQuery::class, [get_called_class()]);
    }

    /**
     * @param null $condition
     * @param array $attributes
     *
     * @param bool $if_condition_is_null
     * @return bool
     * @throws Exception
     */
    public static function deleteAll($condition = NULL, $attributes = [], $if_condition_is_null = false)
    {
        if (empty($condition)) {
            if (!$if_condition_is_null) {
                return false;
            }
            return static::find()->deleteAll();
        }
        $model = static::find()->ifNotWhere($if_condition_is_null)->where($condition);
        if (!empty($attributes)) {
            $model->bindParams($attributes);
        }
        return $model->deleteAll();
    }


    /**
     * @return array
     */
    public function getAttributes()
    {
        return $this->_attributes;
    }

    /**
     * @return array
     */
    public function getOldAttributes()
    {
        return $this->_oldAttributes;
    }

    /**
     * @param $name
     * @param $value
     * @return mixed
     */
    public function setAttribute($name, $value)
    {
        return $this->_attributes[$name] = $value;
    }

    /**
     * @param $name
     * @param $value
     * @return mixed
     */
    public function setOldAttribute($name, $value)
    {
        return $this->_oldAttributes[$name] = $value;
    }

    /**
     * @param array $param
     * @return $this
     * @throws
     */
    public function setAttributes($param)
    {
        if (empty($param) || !is_array($param)) {
            return $this;
        }
        foreach ($param as $key => $val) {
            if ($val === null) {
                continue;
            }
            if (!$this->has($key)) {
                $this->setAttribute($key, $val);
            } else {
                $this->$key = $val;
            }
        }
        return $this;
    }

    public function setOldAttributes($param)
    {
        if (empty($param) || !is_array($param)) {
            return $this;
        }
        foreach ($param as $key => $val) {
            $this->setOldAttribute($key, $val);
        }
        return $this;
    }

    /**
     * @return bool
     * @throws Exception
     */
    public function beforeSave()
    {
        return true;
    }

    /**
     * @param $attributes
     * @param $param
     * @return $this|bool
     * @throws Exception
     */
    private function insert($param, $attributes)
    {
        if (empty($param)) {
            return FALSE;
        }
        $dbConnection = static::getDb();
        $change       = $dbConnection->getSchema()->getChange();
        $sqlBuilder   = $change->insert(static::getTable(), $attributes, $param);

        try {
            $commandExec = $dbConnection->createCommand($sqlBuilder, static::getDbName(), $param);
            if (($lastId = $commandExec->save(true, $this->hasAutoIncrement())) === false) {
                throw new Exception('保存失败.' . $sqlBuilder);
            }
            $this->setAttributes($param);
            if ($this->hasAutoIncrement()) {
                $this->{$this->getAutoIncrement()} = (int)$lastId;
            } else if (static::hasPrimary()) {
                $primary = static::getPrimary();
                if (!isset($param[$primary]) || empty($param[$primary])) {
                    $this->{$primary} = (int)$lastId;
                }
            }
            $this->refresh();
        } catch (Exception $exception) {
            $lastId = false;
        }
        return $lastId;
    }


    /**
     * @param $param
     * @param $condition
     * @param $attributes
     * @return $this|bool
     * @throws Exception
     */
    protected function updateInternal($attributes, $condition, $param)
    {
        if (empty($param)) {
            return true;
        }
        $command = static::getDb();
        $change  = $command->getSchema()->getChange();
        $sql     = $change->update(static::getTable(), $attributes, $condition, $param);

        if (!($command = $command->createCommand($sql, static::getDbName(), $param)->save(false, $this->hasAutoIncrement()))) {
            $result = $this->addError($this->getLastError());
        } else {
            $result = static::populate($this->_attributes);
        }
        return $result;
    }

    /**
     * @param null $data
     * @return bool|mixed|ActiveRecord
     * @throws Exception
     */
    public function save($data = NULL)
    {
        $this->setAttributes($data);
        if (empty($data)) {
            if (!$this->validator($this->rules())) {
                return false;
            }
            if (!$this->beforeSave()) {
                return false;
            }
        }

        $format            = static::getColumns()->format(static::getDbName());
        $this->_attributes = array_merge($format, $this->_attributes);
        static::getDb()->enablingTransactions();

        [$attributes, $condition, $param] = $this->filtration_and_separation();
        if (($primary = static::getPrimary()) !== null) {
            $condition = [$primary => $this->getPrValue()];
        }

        if (!$this->getIsCreate()) {
            return $this->updateInternal($param, $condition, $attributes);
        }
        return $this->insert($attributes, $param);
    }


    /**
     * @param array $rule
     * @return bool
     * @throws Exception
     */
    public function validator(array $rule): bool
    {
        if (empty($rule)) return true;
        $validate = $this->resolve($rule);
        if (!$validate->validation()) {
            return $this->addError($validate->getError(), 'mysql');
        } else {
            return TRUE;
        }
    }

    /**
     * @param $rule
     * @return Validator
     * @throws Exception
     */
    private function resolve($rule): Validator
    {
        $validate = Validator::getInstance();
        $validate->setParams($this->_attributes);
        $validate->setModel($this);
        foreach ($rule as $Key => $val) {
            $field = array_shift($val);
            if (empty($val)) {
                continue;
            }
            $validate->make($field, $val);
        }
        return $validate;
    }

    /**
     * @param string $name
     * @return null
     * @throws Exception
     */
    public function getAttribute(string $name)
    {
        $method = 'get' . ucfirst($name) . 'Attribute';
        if (method_exists($this, $method)) {
            return $this->$method($this->_attributes[$name]);
        }
        return $this->_attributes[$name] ?? null;
    }


    /**
     * @return array
     * @throws Exception
     */
    private function filtration_and_separation(): array
    {
        $_tmp      = [];
        $condition = [];
        $columns   = static::getColumns();
        foreach ($this->_attributes as $key => $val) {
            if ($val === NULL) continue;
            $oldValue = $this->_oldAttributes[$key] ?? null;
            if ($val !== $oldValue) {
                $_tmp[$key] = $columns->fieldFormat($key, $val, static::getDbName());
            } else {
                $condition[$key] = $val;
            }
        }
        return [$_tmp, $condition, array_keys($_tmp)];
    }


    /**
     * @param $name
     * @param $value
     */
    public function setRelate($name, $value)
    {
        $this->_relate[$name] = $value;
    }

    /**
     * @param array $relates
     */
    public function setRelates(array $relates)
    {
        if (empty($relates)) {
            return;
        }
        foreach ($relates as $key => $val) {
            $this->setRelate($key, $val);
        }
    }

    /**
     * @return array
     */
    public function getRelates()
    {
        return $this->_relate;
    }


    /**
     * @return Relation
     */
    public function getRelation()
    {
        return $this->_relation;
    }


    /**
     * @param $name
     * @return mixed|null
     */
    public function getRelate($name)
    {
        if (!isset($this->_relate[$name])) {
            return NULL;
        }
        return $this->_relate[$name];
    }


    /**
     * @param $attribute
     * @return bool
     * @throws Exception
     */
    public function has($attribute)
    {
        if ($attribute == 'attributes') {
            return false;
        }
        $format = static::getColumns()->format(static::getDbName());
        return array_key_exists($attribute, $format);
    }

    /**ƒ
     * @return string
     * @throws Exception
     */
    public static function getTable()
    {
        $tablePrefix = static::getDb()->tablePrefix;

        $table = static::tableName();

        if (strpos($table, $tablePrefix) === 0) {
            return '`' . static::getDb()->database . '`.' . $table;
        }

        if (empty($table)) {
            $class = preg_replace('/model\\\/', '', get_called_class());
            $table = lcfirst($class);
        }

        $table = trim($table, '{{%}}');
        if ($tablePrefix) {
            $table = $tablePrefix . $table;
        }
        return '`' . static::getDb()->database . '`.' . $table;
    }


    /**
     * @param $attributes
     * @param $changeAttributes
     * @return mixed
     * @throws Exception
     */
    public function afterSave($attributes, $changeAttributes)
    {
        return true;
    }

    /**
     * @return Connection
     * @throws Exception
     */
    public static function getDb()
    {
        return static::setDatabaseConnect('db');
    }

    /**
     * @return mixed
     * @throws Exception
     */
    public function getPrValue()
    {
        return $this->getAttribute(static::getPrimary());
    }

    /**
     * @return static
     */
    public function refresh()
    {
        $this->_oldAttributes = $this->_attributes;
        $this->isNewExample   = false;
        return $this;
    }

    /**
     * @param $name
     * @param $value
     * @throws Exception
     */
    public function __set($name, $value)
    {
        if (!$this->has($name)) {
            parent::__set($name, $value);
        } else {
            $sets = 'set' . ucfirst($name) . 'Attribute';
            if (method_exists($this, $sets)) {
                $value = $this->$sets($value);
            }
            $this->_attributes[$name] = $value;
        }
    }

    /**
     * @param $name
     * @return mixed|null
     * @throws Exception
     */
    public function __get($name)
    {
        $method = 'get' . ucfirst($name);
        if (method_exists($this, $method . 'Attribute')) {
            return $this->{$method . 'Attribute'}($this->_attributes[$name] ?? null);
        }

        if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
            return stripcslashes($this->_attributes[$name]);
        }

        if (isset($this->_relate[$name])) {
            $gets = $this->{$this->_relate[$name]}();
        } else if (method_exists($this, $method)) {
            $gets = $this->$method();
        }

        if (isset($gets)) {
            return $this->resolveClass($gets);
        }

        return parent::__get($name);
    }

    /**
     * @param $name
     * @return mixed|null
     */
    public function __isset($name)
    {
        return $this->_attributes[$name] ?? null;
    }

    /**
     * @param $call
     * @return array|null|ActiveRecord
     * @throws Exception
     */
    private function resolveClass($call)
    {
        if ($call instanceof HasOne) {
            return $call->get();
        } else if ($call instanceof HasMany) {
            return $call->get();
        } else {
            return $call;
        }
    }


    /**
     * @param $offset
     * @return bool
     * @throws Exception
     */
    public function offsetExists($offset): bool
    {
        return $this->has($offset);
    }

    /**
     * @param $offset
     * @return mixed|null
     * @throws Exception
     */
    public function offsetGet($offset): mixed
    {
        return $this->__get($offset);
    }

    /**
     * @param $offset
     * @param $value
     * @throws Exception
     */
    public function offsetSet($offset, $value): void
    {
        $this->__set($offset, $value);
    }

    /**
     * @param $offset
     * @throws Exception
     */
    public function offsetUnset($offset): void
    {
        if (!$this->has($offset)) {
            return;
        }
        unset($this->_attributes[$offset]);
        unset($this->_oldAttributes[$offset]);
        if (isset($this->_relate)) {
            unset($this->_relate[$offset]);
        }
    }

    /**
     * @return array
     */
    public function unset()
    {
        $fields = func_get_args();
        $fields = array_shift($fields);
        if (!is_array($fields)) {
            $fields = explode(',', $fields);
        }

        $array = array_combine($fields, $fields);

        return array_diff_assoc($array, $this->_attributes);
    }


    public static $_bsName;


    /**
     * @param $bsName
     * @return mixed
     * @throws Exception
     */
    public static function setDatabaseConnect($bsName)
    {
        $connection      = \BeReborn::$app->{$bsName};
        static::$_bsName = $connection->database;
        return $connection;
    }


    public static function getDbName()
    {
        return static::$_bsName;
    }


    /**
     * @return Columns
     * @throws Exception
     */
    public static function getColumns()
    {
        return static::getDb()->getSchema()
                     ->getColumns()
                     ->table(static::getTable());
    }

    /**
     * @param array $data
     * @return static
     * @throws
     */
    public static function populate(array $data)
    {
        $model = new static();
        $model->setAttributes(self::parse($data));
        $model->setIsCreate(false);
        $model->refresh();
        return $model;
    }


    /**
     * @param $data
     * @return array
     * @throws Exception
     */
    private static function parse($data)
    {
        return static::getColumns()->populate($data, static::getDbName());
    }
}
